#include <vector>

#include "Tutorial.h"
#include "VulkanFunctions.h"

Tutorial::Tutorial()
{

}

Tutorial::~Tutorial()
{
	if (vulkan_.device != VK_NULL_HANDLE)
	{
		vkDeviceWaitIdle(vulkan_.device);
		vkDestroyDevice(vulkan_.device, nullptr);
	}

	if (vulkan_.instance != VK_NULL_HANDLE)
	{
		vkDestroyInstance(vulkan_.instance, nullptr);
	}

	if (vulkanLibrary_)
	{
		FreeLibrary(vulkanLibrary_);
	}
}

bool Tutorial::OnWindowSizeChanged()
{
	return true;
}

bool Tutorial::Draw()
{
	return true;
}

bool Tutorial::PrepareVulkan()
{
	if (!LoadVulkanLibrary())
		return false;

	if (!LoadExportedEntryPoints())
		return false;

	if (!LoadGlobalLevelEntryPoints())
		return false;

	if (!CreateInstance())
		return false;

	if (!LoadInstanceLevelEntryPoints())
		return false;

	if (!CreateDevice())
		return false;

	if (!LoadDeviceLevelEntryPoints())
		return false;

	if (!GetDeviceQueue())
		return false;

	return true;
}

bool Tutorial::LoadVulkanLibrary()
{
	vulkanLibrary_ = LoadLibrary("vulkan-1.dll");

	if (vulkanLibrary_ == nullptr)
	{
		std::cout << "LoadVulkanLibrary fail" << std::endl;
		return false;
	}
	return true;
}

bool Tutorial::LoadExportedEntryPoints()
{
#define VK_EXPORTED_FUNCTION(fun)									\
	if (!(fun = (PFN_##fun)GetProcAddress(vulkanLibrary_, #fun)))	\
	{																\
		std::cout << "LoadExportedEntryPoints fail" << std::endl;	\
		return false;												\
	}

	#include "ListOfFunctions.inl"
	return true;
}

bool Tutorial::LoadGlobalLevelEntryPoints()
{
#define VK_GLOBAL_LEVEL_FUNCTION(fun)									\
	if (!(fun = (PFN_##fun)vkGetInstanceProcAddr(nullptr, #fun)))		\
	{																	\
		std::cout << "LoadGlobalLevelEntryPoints fail" << std::endl;	\
		return false;													\
	}			

	#include "ListOfFunctions.inl"

	return true;
}

bool Tutorial::CreateInstance()
{
	VkApplicationInfo applicationInfo = {
		VK_STRUCTURE_TYPE_APPLICATION_INFO,
		nullptr,
		"Learn Vulkan",
		VK_MAKE_VERSION(1, 0, 0),
		"Custom Engine",
		VK_MAKE_VERSION(1, 0, 0),
		VK_API_VERSION
	};

	VkInstanceCreateInfo instanceCreateInfo = {
		VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO,
		nullptr,
		0,
		&applicationInfo,
		0,
		nullptr,
		0,
		nullptr
	};

	if (vkCreateInstance(&instanceCreateInfo, nullptr, &vulkan_.instance) != VK_SUCCESS)
	{
		std::cout << "CreateInstance fail" << std::endl;
		return false;
	}

	return true;
}

bool Tutorial::LoadInstanceLevelEntryPoints()
{
#define VK_INSTANCE_LEVEL_FUNCTION(fun)										\
	if (!(fun = (PFN_##fun)vkGetInstanceProcAddr(vulkan_.instance, #fun)))	\
	{																		\
		std::cout << "LoadInstanceLevelEntryPoints fail" << std::endl;		\
		return false;														\
	}

	#include "ListOfFunctions.inl"
	return true;
}

bool Tutorial::CreateDevice()
{
	uint32_t numDevices = 0u;
	if (vkEnumeratePhysicalDevices(vulkan_.instance, &numDevices, nullptr) != VK_SUCCESS || numDevices == 0)
	{
		std::cout << "Zero Devices" << std::endl;
		return false;
	}

	std::vector<VkPhysicalDevice> physicalDevices(numDevices);
	if (vkEnumeratePhysicalDevices(vulkan_.instance, &numDevices, &physicalDevices[0]) != VK_SUCCESS)
	{
		std::cout << "Enumerate Devices fail" << std::endl;
		return false;
	}

	VkPhysicalDevice selectedPhysicalDevice = VK_NULL_HANDLE;
	uint32_t selectedQueueFamilyIndex = UINT32_MAX;
	for (uint32_t i = 0; i < numDevices; ++i)
	{
		if (CheckPhysicalDeviceProperties(physicalDevices[i], selectedQueueFamilyIndex))
		{
			selectedPhysicalDevice = physicalDevices[i];
			break;
		}
	}

	std::vector<float> queuePriorities{ 1.0 };
	VkDeviceQueueCreateInfo queueCreateInfo = {
		VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO,
		nullptr,
		0,
		selectedQueueFamilyIndex,
		static_cast<uint32_t>(queuePriorities.size()),
		&queuePriorities[0]
	};

	VkDeviceCreateInfo deviceCreateInfo = {
		VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO,
		nullptr,
		0,
		1,
		&queueCreateInfo,
		0,
		nullptr,
		0,
		nullptr,
		nullptr
	};

	if (vkCreateDevice(selectedPhysicalDevice, &deviceCreateInfo, nullptr, &vulkan_.device) != VK_SUCCESS)
	{
		std::cout << "Could not create Vulkan device!" << std::endl;
		return false;
	}

	vulkan_.queueFamilyIndex = selectedQueueFamilyIndex;

	return true;
}

bool Tutorial::CheckPhysicalDeviceProperties(VkPhysicalDevice physical_device, uint32_t & queue_family_index)
{
	VkPhysicalDeviceFeatures deviceFeature;
	VkPhysicalDeviceProperties deviceProperty;

	vkGetPhysicalDeviceFeatures(physical_device, &deviceFeature);
	vkGetPhysicalDeviceProperties(physical_device, &deviceProperty);

	uint32_t majorVersion = VK_VERSION_MAJOR(deviceProperty.apiVersion);
	uint32_t minorVersion = VK_VERSION_MINOR(deviceProperty.apiVersion);
	uint32_t patchVersion = VK_VERSION_PATCH(deviceProperty.apiVersion);

	if ((majorVersion < 1) && (deviceProperty.limits.maxImageDimension2D < 4096))
	{
		std::cout << "Physical device " << deviceProperty.deviceName << " doesn't support required parameters!" << std::endl;
		return false;
	}

	uint32_t queueFamilyCount = 0;
	vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queueFamilyCount, nullptr);
	if (queueFamilyCount == 0)
	{
		std::cout << "Physical device " << deviceProperty.deviceName << " doesn't have any queue families!" << std::endl;
		return false;
	}

	std::vector<VkQueueFamilyProperties> queueFamilyProperties(queueFamilyCount);
	vkGetPhysicalDeviceQueueFamilyProperties(physical_device, &queueFamilyCount, &queueFamilyProperties[0]);
	for (uint32_t i = 0; i < queueFamilyCount; ++i)
	{
		if ((queueFamilyProperties[i].queueCount > 0) && (queueFamilyProperties[i].queueFlags & VK_QUEUE_GRAPHICS_BIT))
		{
			queue_family_index = i;
			return true;
		}
	}

	std::cout << "Could not find queue family with required properties on physical device " << deviceProperty.deviceName << "!" << std::endl;
	return false;
}

bool Tutorial::LoadDeviceLevelEntryPoints()
{
#define VK_DEVICE_LEVEL_FUNCTION(fun)									\
	if (!(fun = (PFN_##fun)vkGetDeviceProcAddr(vulkan_.device, #fun)))				\
	{																	\
		std::cout << "LoadDeviceLevelEntryPoints fail" << std::endl;	\
		return false;													\
	}
	
	#include "ListOfFunctions.inl"
	
	return true;
}

bool Tutorial::GetDeviceQueue()
{
	vkGetDeviceQueue(vulkan_.device, vulkan_.queueFamilyIndex, 0, &vulkan_.queue);
	return true;
}
